package nl.utwente.ewi.hmi.multitouch.drivers.mouse;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;

import nl.utwente.ewi.hmi.multitouch.SurfaceType;
import nl.utwente.ewi.hmi.multitouch.Tangible;
import nl.utwente.ewi.hmi.multitouch.TouchDeviceInfo;
import nl.utwente.ewi.hmi.multitouch.analysis.Segment;
import nl.utwente.ewi.hmi.multitouch.io.TouchStream;
import nl.utwente.ewi.hmi.multitouch.io.TouchStreamProvider;

/**
 * The MouseTouchStreamProvider is a simple {@linkplain TouchStreamProvider} which offers one single {@linkplain TouchStream}
 * representing interaction with a mouse on a {@link JFrame}. 
 * 
 * @author Michiel Hakvoort
 * @version 1.0
 */
public class MouseTouchStreamProvider implements TouchStreamProvider {

	private static final List<TouchDeviceInfo> infoList;
	
	private static final TouchDeviceInfo mouseTouchDeviceInfo;

	private static final TouchStream mouseTouchStream;
	
	private static final Dimension size = new Dimension(200, 200);
	
	private static final MouseTouchStreamProvider instance;
	
	private Set<Object> owners = null;
	
	private MouseTouchStreamProvider() {
		this.owners = new HashSet<Object>();
	}
	
	public static MouseTouchStreamProvider getMouseTouchStreamProvider() {
		return instance;
	}
	
	static {
		instance = new MouseTouchStreamProvider();
		mouseTouchDeviceInfo = new TouchDeviceInfo(size, "mouse");

		List<TouchDeviceInfo> list = new ArrayList<TouchDeviceInfo>();
		list.add(mouseTouchDeviceInfo);

		infoList = Collections.unmodifiableList(list);

		mouseTouchStream = new MouseTouchStream(mouseTouchDeviceInfo);


	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<TouchDeviceInfo> getTouchDeviceInfoList() {
		return infoList;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public TouchStream getTouchStream(TouchDeviceInfo deviceInfo) {
		if(!mouseTouchDeviceInfo.equals(deviceInfo)) {
			return null;
		}

		return mouseTouchStream;
	}

	private static class MouseTouchStream extends TouchStream {

		private Set<Object> actors = null;
		
		{
			actors = new HashSet<Object>();
		}
		
		Frame[] frames = null;
		
		boolean isOpen = false;

		JFrame frame = null;

		int brushSize = 16;
		int x;
		int y;
		int dx;
		int dy;

		boolean hasPress = false;
		
		boolean frameReady = false;
		
		TouchType[] touchTypeMap = new TouchType[size.height * size.width];
		Tangible[] tangibleMap = new Tangible[size.height * size.width];
		
		Tangible touch = new Tangible() {

			Object actor = null;
			
			
			{
				actor = new Object();
				MouseTouchStream.this.actors.add(actor);
				
			}
			
			SurfaceType type = SurfaceType.UNKNOWN;
			
			@Override
			public Object getActor() {
				return actor;
			}

			@Override
			public SurfaceType getSurfaceType() {
				return type;
			}
			
			@Override
			public boolean canJoinInto(Tangible tangible) {
				return false;
			}
			
			@Override
			public boolean canSplitInto(Tangible tangible) {
				return false;
			}
			@Override
			public boolean isUnique() {
				return false;
			}

		};
		
		int[] target = new int[size.width * size.height];
		
		BufferedImage bi = null;
		Graphics2D g = null;
		
		public MouseTouchStream(TouchDeviceInfo touchDeviceInfo) {
			super(touchDeviceInfo);

			ColorModel colorModel = new DirectColorModel(32, 0x00FF0000, 0x0000FF00, 0x000000FF);

			SampleModel sample = new SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, size.width, size.height, new int[] { 0x00FF0000, 0x0000FF00, 0x000000FF});
			
			DataBuffer db = new DataBufferInt(this.target, size.width * size.height);
			
			WritableRaster raster = Raster.createWritableRaster(sample, db, new Point(0, 0));
			
			bi = new BufferedImage(colorModel, raster, false, null);
			
			g = (Graphics2D)bi.getGraphics();
			
			g.setColor(Color.black);
			
			g.fillRect(0, 0, bi.getWidth(), bi.getHeight());
			
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
			
			JPanel panel = new JPanel() {
				private static final long serialVersionUID = 6966702932523190797L;

				@Override
				public void paint(Graphics g) {
					g.drawImage(bi, 0, 0, this.getWidth(), this.getHeight(), 0, 0, bi.getWidth(), bi.getHeight(), null);
				}
			};
			
			panel.setPreferredSize(size);
			panel.setBorder(new EmptyBorder(5, 5, 5, 5));

			panel.addMouseMotionListener(new MouseMotionAdapter() {
				@Override
				public void mouseDragged(MouseEvent e) {
					x = e.getX();
					y = e.getY();
					hasPress = true;

					updateFrame();
				}
				
			});

			panel.addMouseListener(new MouseAdapter() {
				@Override
				public void mouseExited(MouseEvent e) {
					hasPress = false;
					
					updateFrame();
				}
				
				@Override
				public void mouseReleased(MouseEvent e) {
					hasPress = false;
					
					updateFrame();
					
				}
			});

			panel.addMouseWheelListener(new MouseWheelListener() {

				@Override
				public void mouseWheelMoved(MouseWheelEvent e) {
					if(e.getWheelRotation() < 0) {
						brushSize+=2;
					} else if(e.getWheelRotation() > 0) {
						brushSize-=2;
					}
					brushSize = Math.max(4, Math.min(64, brushSize));
					
					if(hasPress) {
						updateFrame();
					}
				}
				
			});
			
			
			frame = new JFrame();
			frame.addKeyListener(new KeyAdapter() {
				@Override
				public void keyPressed(KeyEvent e) {
					if(e.getKeyCode() == KeyEvent.VK_LEFT) {
						dx++;
					} else if(e.getKeyCode() == KeyEvent.VK_RIGHT) {
						dx--;
					} else if(e.getKeyCode() == KeyEvent.VK_UP) {
						dy++;
					} else if(e.getKeyCode() == KeyEvent.VK_DOWN) {
						dy--;
					}

					dx = Math.max(0, Math.min(64, dx));
					dy = Math.max(0, Math.min(64, dy));
					
					updateFrame();
				}
			});
			
			frame.addWindowListener(new WindowAdapter() {
				@Override
				public void windowClosing(WindowEvent e) {
					frame.dispose();
					synchronized(MouseTouchStream.this) {
						MouseTouchStream.this.notifyAll();
					}
				}
			});
			frame.getContentPane().setLayout(new BorderLayout());
			frame.getContentPane().add(panel);
			frame.setResizable(false);

			frame.pack();
			
			frames = new Frame[] { new Frame(size) };
		}

		void updateFrame() {
			clearImage();
			drawPen();

			frame.repaint();

			for(int i = 0; i < target.length; i++) {
				if(target[i] != 0x00) {
					touchTypeMap[i] = TouchType.POSITIVE;
					tangibleMap[i] = touch;
				} else {
					touchTypeMap[i] = TouchType.NEGATIVE;
					tangibleMap[i] = null;
				}
			}


			synchronized(this) {
				frames[0].writeTangibleMap(tangibleMap);
				frames[0].writeTouchTypeMap(touchTypeMap);
				this.frameReady = true;
				this.notifyAll();
			}
		}
		
		@Override
		public synchronized boolean close() {
			if(!this.isOpen) {
				return false;
			}

			this.isOpen = false;
			
			frame.setVisible(false);

			return true;
		}

		
		/*
		@Override
		public Frame[] getFrames() throws IOException {
			synchronized(this) {
				while((!this.frameReady) && frame.isVisible()) {
					try {
						this.wait();
					} catch (InterruptedException e) {
					}
				}
				this.frameReady = false;
			}

			if(!frame.isVisible()) {
				throw new IOException("Input died");
			}

			return frames;
		}*/

		@Override
		public synchronized boolean isOpen() {
			return this.isOpen;
		}

		@Override
		public synchronized boolean open() {
			if(this.isOpen) {
				return false;
			}

			this.isOpen = true;
			
			frame.setVisible(true);
			
			return true;
		}

		
		
		void clearImage() {
			g.setColor(Color.black);
			g.fillRect(0, 0, bi.getWidth(), bi.getHeight());
			frame.repaint();
		}

		void drawPen() {
			if(!hasPress) {
				return;
			}
			g.setColor(Color.white);
			g.fillOval((dx + x) - (brushSize / 2), (dy + y) - (brushSize / 2), brushSize, brushSize);
			if(dx > 0 || dy > 0) {
				g.fillOval((x - dx) - (brushSize / 2), (y - dy)- (brushSize / 2), brushSize, brushSize);	
			}

		}

		@Override
		public Set<Object> getActors() {
			return Collections.unmodifiableSet(this.actors);
		}

		@Override
		public void read(Set<Segment> segments) throws IOException {
			// TODO Auto-generated method stub
			
		}
	}

}
